package com.alibaba.algo.components.permission.auth;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Enumeration;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import com.alibaba.algo.components.permission.auth.enums.AuthMethod;
import com.alibaba.algo.components.permission.auth.enums.AuthParam;
import com.alibaba.algo.components.permission.auth.enums.AuthScheme;
import com.alibaba.algo.components.permission.commons.GlobalResultStatus;
import com.alibaba.algo.components.permission.service.AppInfoService;
import com.alibaba.algo.components.permission.service.TokenService;
import com.alibaba.algo.core.contant.SysConstant;
import com.alibaba.algo.core.exception.BaseException;
import com.alibaba.algo.core.resp.BaseResult;
import com.alibaba.algo.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;

/**
 * @Description: 检查授权信息拦截器
 * @author: malin
 * @date: 2020-12-15 11:36:11
 */
@Component
@Slf4j
public class CheckAuthHandlerInterceptor extends HandlerInterceptorAdapter {
	@Autowired
	private TokenService tokenService;
	@Autowired
	private AppInfoService appInfoService;
	@Resource
	private RedisTemplate<String, String> permissionRedisTemplate;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// 如果是静态资源直接放过
		if (handler instanceof ResourceHttpRequestHandler) {
			return true;
		}
		if (handler instanceof HandlerMethod) {
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			CheckAuth auth = handlerMethod.getMethodAnnotation(CheckAuth.class);

			if (!checkAuthMethod(request, response, auth)) {
				return false;
			}

			if (!checkAuthScheme(request, response, auth)) {
				return false;
			}

			if (!checkAuthParam(request, response, auth)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 检查授权参数
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param auth     授权信息
	 * @return 检查是否通过
	 */
	private boolean checkAuthParam(HttpServletRequest request, HttpServletResponse response, CheckAuth auth) {
		if (auth == null || auth.param().length == 0) {
			return true;
		}
		// 检查timestamp参数
		String timestamp = request.getHeader(SysConstant.SYS_PERMISSION_AUTH_TIMESTAMP);
		if (StringUtil.isEmpty(timestamp)) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.AUTH_MISSING.getCode(),
					GlobalResultStatus.AUTH_MISSING.getMsg()));
			return false;
		}
		if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 10 * 60 * 1000) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.TIMESTAMP_EXPIRED.getCode(),
					GlobalResultStatus.TIMESTAMP_EXPIRED.getMsg()));
			return false;
		}

		// 检查appId合法性
		String appId = request.getHeader(SysConstant.SYS_PERMISSION_AUTH_APPID);
		if (StringUtil.isEmpty(appId)) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.AUTH_MISSING.getCode(),
					GlobalResultStatus.AUTH_MISSING.getMsg()));
			return false;
		}

		String appSecret = appInfoService.getAppSecret(Long.parseLong(appId));
		if (appSecret == null) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.APPID_ERROR.getCode(),
					GlobalResultStatus.APPID_ERROR.getMsg()));
			return false;
		}

		// 检查配置的参数
		for (AuthParam authParam : auth.param()) {
			switch (authParam) {
			case ACCESS_TOKEN:// 检查访问凭证
				if (!checkAccessToken(request, response)) {
					return false;
				}
				break;
			case NONCE:// 检查NONCE
				if (!checkNonce(request, response)) {
					writeResponse(response, BaseResult.buildError(GlobalResultStatus.NONCE_ERROR.getCode(),
							GlobalResultStatus.NONCE_ERROR.getMsg()));
					return false;
				}
				break;
			case SIGN:// 检查签名
				String sign = request.getHeader(SysConstant.SYS_PERMISSION_AUTH_SIGNATURE);
				if (StringUtil.isEmpty(sign)) {
					writeResponse(response, BaseResult.buildError(GlobalResultStatus.AUTH_MISSING.getCode(),
							GlobalResultStatus.AUTH_MISSING.getMsg()));
					return false;
				}
				String checkedSign = getSign(request, appSecret, sign);
				if (!checkedSign.equals(sign)) {
					writeResponse(response, BaseResult.buildError(GlobalResultStatus.SIGNATURE_ERROR.getCode(),
							GlobalResultStatus.SIGNATURE_ERROR.getMsg()));
					return false;
				}
				break;
			default:
			}
		}

		return true;
	}

	/**
	 * @Description: 检查nonce(防止重复提交)
	 */
	private boolean checkNonce(HttpServletRequest request, HttpServletResponse response) {
		String nonce = request.getHeader(SysConstant.SYS_PERMISSION_AUTH_NONCE);
		if (StringUtil.isEmpty(nonce)) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.AUTH_MISSING.getCode(),
					GlobalResultStatus.AUTH_MISSING.getMsg()));
			return false;
		}
		String appId = request.getHeader(SysConstant.SYS_PERMISSION_AUTH_APPID);
		String key = appId + ":" + nonce;
		if (permissionRedisTemplate.hasKey(key)) {
			return false;
		}
		permissionRedisTemplate.opsForValue().set(key, nonce, 10 * 60 * 1000, TimeUnit.MILLISECONDS);
		return true;
	}

	/**
	 * 检查ccessToken
	 * @param accessToken
	 * @param appId
	 * @param response
	 * @return
	 */
	private boolean checkAccessToken(HttpServletRequest request, HttpServletResponse response) {
		// 获取token
		String accessToken = tokenService.getTokenFromHttpServletRequest(request);
		if (StringUtil.isEmpty(accessToken)) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.AUTH_MISSING.getCode(),
					GlobalResultStatus.AUTH_MISSING.getMsg()));
			return false;
		}
		// 校验token合法性
		boolean result = false;
		try {
			result = tokenService.checkToken(accessToken);
		} catch (BaseException baseException) {
			log.error("token:{} 错误信息：{}", accessToken, baseException);
		} catch (Exception e) {
			log.error("token:{} 错误信息：{}", accessToken, e);
		}
		if (!result) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.ACCESS_TOKEN_ERROR.getCode(),
					GlobalResultStatus.ACCESS_TOKEN_ERROR.getMsg()));
			return false;
		}
		return true;
	}

	/**
	 * 检查授权Scheme
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param auth     授权信息
	 * @return 检查是否通过
	 */
	private boolean checkAuthScheme(HttpServletRequest request, HttpServletResponse response, CheckAuth auth) {
		if (auth == null || auth.scheme().length == 0) {
			return true;
		}

		String requestUrl = getRequestUrl(request);
		URI uri = null;
		try {
			uri = new URI(requestUrl);
		} catch (URISyntaxException e) {
			writeResponse(response, BaseResult.buildError(GlobalResultStatus.REQUEST_URL_ERROR.getCode(),
					GlobalResultStatus.REQUEST_URL_ERROR.getMsg()));
			return false;
		}

		String requestScheme = uri.getScheme();

		for (AuthScheme authScheme : auth.scheme()) {
			if (authScheme.toString().equalsIgnoreCase(requestScheme)) {
				return true;
			}
		}
		writeResponse(response, BaseResult.buildError(GlobalResultStatus.REQUEST_SCHEME_ERROR.getCode(),
				GlobalResultStatus.REQUEST_SCHEME_ERROR.getMsg()));
		return false;
	}

	/**
	 * 检查授权方法
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param auth     授权信息
	 * @return 检查是否通过
	 */
	private boolean checkAuthMethod(HttpServletRequest request, HttpServletResponse response, CheckAuth auth) {
		if (auth == null || auth.method().length == 0) {
			return true;
		}
		String requestMethod = request.getMethod();
		for (AuthMethod authMethod : auth.method()) {
			if (authMethod.toString().equalsIgnoreCase(requestMethod)) {
				return true;
			}
		}
		writeResponse(response, BaseResult.buildError(GlobalResultStatus.REQUEST_METHOD_ERROR.getCode(),
				GlobalResultStatus.REQUEST_METHOD_ERROR.getMsg()));
		return false;
	}

	/**
	 * 根据请求信息生成签名（query、form、body,header的顺序进行）
	 * @param request   请求对象
	 * @param appSecret APP密钥
	 * @param sign      app传递过来的签名
	 * @return 签名值
	 */
	private String getSign(HttpServletRequest request, String appSecret, String sign) {
		Map<String, String> params = new TreeMap<String, String>();
		for (Enumeration<String> paramNames = request.getParameterNames(); paramNames.hasMoreElements();) {
			String paramName = paramNames.nextElement();
			params.put(paramName, request.getParameter(paramName));
		}
		// 取body
		String body = null;
		try {
			body = IOUtils.toString(request.getReader());
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (StringUtil.isNotEmpty(body)) {
			JSONObject bodyJson = JSON.parseObject(body);
			Set<String> set = bodyJson.keySet();
			for (String key : set) {
				params.put(key, bodyJson.getString(key));
			}
		}
		// 获取header（token，timestamp，appId，nonce）
		if(StringUtil.isNotEmpty(request.getHeader(SysConstant.SYS_PERMISSION_JWT_TOKEN))) {
			params.put(SysConstant.SYS_PERMISSION_JWT_TOKEN, request.getHeader(SysConstant.SYS_PERMISSION_JWT_TOKEN));
		}
		params.put(SysConstant.SYS_PERMISSION_AUTH_APPID, request.getHeader(SysConstant.SYS_PERMISSION_AUTH_APPID));
		params.put(SysConstant.SYS_PERMISSION_AUTH_TIMESTAMP,
				request.getHeader(SysConstant.SYS_PERMISSION_AUTH_TIMESTAMP));
		params.put(SysConstant.SYS_PERMISSION_AUTH_NONCE, request.getHeader(SysConstant.SYS_PERMISSION_AUTH_NONCE));

		StringBuilder buf = new StringBuilder();
		buf.append(request.getMethod()).append(getRequestUrl(request));
		for (Entry<String, String> param : params.entrySet()) {
			buf.append(param.getKey()).append("=").append(param.getValue()).append("&");
		}
		buf.append(appSecret);
		String md5 = DigestUtils.md5Hex(buf.toString());
		log.info("Url 计算signature:{}", md5);
		if (!md5.equals(sign)) {
			log.error("sign:" + sign + "##" + buf.toString());
		}
		return md5;
	}

	/**
	 * 获取客户端请求地址
	 * @param request 请求对象
	 * @return 客户端请求地址
	 */
	private String getRequestUrl(HttpServletRequest request) {
		// 从反向代理服务器获取客户端请求地址，需要反向代理服务器配置此参数
		String requestUrl = request.getHeader("X-Request-URI");
		if (requestUrl == null) {
			// 从Servlet服务器获取客户端请求地址
			requestUrl = request.getRequestURL().toString();
		}
		return requestUrl;
	}

	/**
	 * 写响应数据
	 * @param response 响应对象
	 * @param result   响应结果
	 */
	private void writeResponse(HttpServletResponse response, Object result) {
		response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
		ObjectMapper mapper = new ObjectMapper();
		try {
			response.getWriter().write(mapper.writeValueAsString(result));
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}
}
